package com.ndkj.blog.aspect;

import com.ndkj.blog.constant.BlogConstant;
import com.ndkj.blog.pojo.entity.IpLog;
import com.ndkj.blog.service.impl.BlogServiceImpl;
import com.ndkj.blog.utils.RequestUtils;
import com.ndkj.blog.utils.GetAttribution;
import com.ndkj.blog.utils.LogWriterUtils;
import com.ndkj.blog.utils.TimeFormatUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;

/**
 * @program: Blog
 * @description: 日志记录切面类
 * @author: liuyong
 * @create: 2021-08-09 22:48
 **/
@Aspect
@Component
@Slf4j
public class AccessLog {

    @Autowired
    private BlogServiceImpl blogService;
    @Autowired
    private LogWriterUtils logWriterUtils;

    /**
     * 存储ip访问信息的list，每隔一定时间将list中的对象写入到数据库
     * 请注意：这里的ipLogs定义为final不会影响ipLogs对象的add或remove操作数据，final约束的仅仅是ipLogs被初始化的对象
     * 例如此处ipLogs new的是new Vector<>(),那么经过final定义后，不能将该ipLogs再被new为别的类对象
     */
    final Vector<IpLog> ipLogs = new Vector<>();
    String ip;

    /**
     * 这个方法在请求之前执行，不直接将访问记录写入数据库，而是将请求的基本信息加入到list中，速度快
     * 为什么要这样做？
     * 调用过程被定义为了@Before，必须先完成该方法才能访问页面，通过api调取归属地的过程十分消耗时间，如果在每次访问前都写一次数据库会影响访问速度
     * 并且api调用可能会发生未知错误，假如api调用过程失败或某些时候api反馈时间较长，会导致页面无法访问的问题
     */
    @Before("execution(* com.ndkj.blog.controller.*.*(..))")
    public void getIPArea() throws IOException, InterruptedException {
        ip = RequestUtils.getIpAddr();
        IpLog ipLog = new IpLog(1, new Date(), ip, "暂无", RequestUtils.getAccessPath());
        ipLogs.add(ipLog);
        log.info(ip + " 请求成功：" + ipLog.toString());
    }

    /**
     * 这个方法将list中对象的ipAttribute调用api查询出ip归属地并且更新list中的ipAttribute
     * 再将list中的全部访问记录写入到数据库
     * 这个方法执行慢，并且有可能会报错,因此选择使用定时任务在每隔半小时执行一次.
     * 在写入数据库时进行访问会抛出并发异常，因此给ipLogs在遍历写入时加锁
     * cron表达式：秒 分 时 日 月 周几（0，7都代表周日）
     * 请注意：
     * 1.如果长时间没有对ipLogs对象进行清空操作，在访问量大的时候会导致ipLogs对象占用内存过大的问题
     * 2.请根据自己的访问情况合理设置定时任务执行周期，及时清理链表元素
     * 3.这里的ip归属地查询api未来可能会失效，失效后请在这个方法中改变可用的ip查询api,并截取所需的归属地字段
     */
    // @Scheduled(cron = "*/5 * * * * ?")   // 5s执行一次，测试用
    // @Scheduled(cron = "0 0/30 * * * ?")  // 30min执行一次
    @Scheduled(cron = "0 0/5 * * * ?")      // 5min执行一次
    public void timingTask() throws IOException {
        HashMap<String, Integer> times4Attributions = new HashMap<>(8);
        //给list对象加锁，防止在遍历写入数据库时访问导致的并发异常
        synchronized (ipLogs) {
            for (IpLog ipLog : ipLogs) {
                //请求api得到的json回复
                String response = new GetAttribution().sendGet(BlogConstant.Attribution.URL + ip + "&json=true");
                //截取其中的ip归属地信息
                String ipAttribute = response.substring(response.indexOf("\"addr\":\"") + 8, response.indexOf("\",\"regionNames\""));
                ipLog.setIpAttribution(ipAttribute);
                Integer flag = blogService.insertLog(ipLog);
                if (StringUtils.isNotBlank(ipAttribute)) {
                    times4Attributions.put(ipAttribute, times4Attributions.get(ipAttribute) == null ? 1 : times4Attributions.get(ipAttribute) + 1);
                }
                String currentAccess = flag == 1 ?
                        "[" + TimeFormatUtils.TimeParse(new Date()) + "]:访问记录写入成功--" + ipLog :
                        "[" + TimeFormatUtils.TimeParse(new Date()) + "]:访问记录写入异常--" + ipLog;
                logWriterUtils.write(currentAccess);
            }
            if (!CollectionUtils.isEmpty(times4Attributions)) {
                StringBuilder logStr = new StringBuilder();
                for (Map.Entry<String, Integer> entry : times4Attributions.entrySet()) {
                    logStr.append(entry.getKey().trim()).append(entry.getValue()).append("次").append(",");
                }
                logStr.deleteCharAt(logStr.length() - 1);
                log.info("[定时任务]访问数据写入数据库成功，本次写入 " + ipLogs.size() + " 条记录," + "[" + logStr + "]");
            } else {
                log.info("[定时任务]访问数据写入数据库成功，本次写入 " + ipLogs.size() + " 条记录");
            }
            ipLogs.removeAllElements();
        }
    }

}
